package microcredit.gui.table;

import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;

import javax.naming.ldap.SortKey;
import javax.swing.DefaultCellEditor;
import javax.swing.DefaultRowSorter;
import javax.swing.ImageIcon;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JTable;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.RowSorter;
import javax.swing.SortOrder;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.event.TableModelEvent;
import javax.swing.plaf.UIResource;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableModel;

import microcredit.gui.util.ExceptionDialog;
import microcredit.gui.util.FormatUtilities;

import com.jidesoft.comparator.ComparableComparator;

public class BeanTable<E> extends JTable {

	public enum SelectionMode {
		DEFAULT, SINGLE, MULTI
	}
	
	private static final long serialVersionUID = -4036051845396331704L;

	private ImageIcon icon;
	
	private final IconCellRenderer iconRenderer = new IconCellRenderer();
	private final BeanCellRenderer beanRenderer = new BeanCellRenderer();
	private final BooleanRenderer booleanRenderer = new BooleanRenderer();
	private final BooleanEditor booleanEditor = new BooleanEditor();
	
	private BeanTableModel<E> beanModel;
	
	private boolean hoverEffect = true;
	private Color lastLineColor = null;
	
	
	public BeanTable() {
		setSelectionBackground(Color.decode("#f9ce07"));
		setSelectionForeground(Color.BLACK);
		
		setRowHeight(23);
		setDefaultEditor(String.class, new BeanTableStringEditor());
		setDefaultEditor(BigDecimal.class, new BeanTableBigDecimalEditor());
		setDefaultEditor(Boolean.class, new BooleanEditor());
		setDefaultRenderer(Boolean.class, new BooleanRenderer());
		addFocusListener(new FocusAdapter() {
			@Override
			public void focusLost(FocusEvent e) {
//				if (getCellEditor() != null) {
//					getCellEditor().stopCellEditing();
//				}
				super.focusLost(e);
			}
		});
		setAutoCreateRowSorter(true);
		
		getInputMap().remove(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0));
	}
	
	public void setLastLineColor(Color lastLineColor) {
		this.lastLineColor = lastLineColor;
	}
	
	public Color getLastLineColor() {
		return lastLineColor;
	}
	
	public void setCheckBoxSelectionMode(SelectionMode selectionMode) {
		getBeanModel().setSelectionMode(selectionMode);
		updateSelectionMode(selectionMode);
	}
	
	public SelectionMode getCheckBoxSelectionMode() {
		return getBeanModel().getSelectionMode();
	}
	
	private final SelectionAdapter selectionAdapter = new SelectionAdapter();
	
	private void updateSelectionMode(SelectionMode mode) {
		if (mode == SelectionMode.DEFAULT) {
			removeMouseMotionListener(selectionAdapter);
			removeMouseListener(selectionAdapter);
			removeKeyListener(selectionAdapter);
		} else {
			setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
			addMouseMotionListener(selectionAdapter);
			addMouseListener(selectionAdapter);
			addKeyListener(selectionAdapter);
		}
	}
	
	@Override
	public void tableChanged(TableModelEvent e) {
		int[] rows = getSelectedRows();
		List<E> selectedObjects = new ArrayList<E>();
		for (int row : rows) {
			try {
				selectedObjects.add(getBeanModel().getBeans().get(convertRowIndexToModel(row)));
			} catch (Exception e1) {
				// ignore exceptions
			}
		}
		super.tableChanged(e);
		if (e.getFirstRow() == TableModelEvent.HEADER_ROW) {
			updateColumnData();
		}
		
		getSelectionModel().clearSelection();
		for (E object : selectedObjects) {
			int modelIndex = getBeanModel().getBeans().indexOf(object);
			if (modelIndex != -1) {
				int viewRow = convertRowIndexToView(modelIndex);
				getSelectionModel().addSelectionInterval(viewRow, viewRow);
			}
		} 
		
	}
	
	@SuppressWarnings("unchecked")
	public void setModel(TableModel model) {
		if (model instanceof BeanTableModel) {
			beanModel = (BeanTableModel<E>)model;
		}
		super.setModel(model);
		updateRowSorter();
	}
	
	public void load(List<E> data) {
		beanModel.load(data);
	}
	
	private void updateColumnData() {
		if (beanModel == null) {
			return;
		}
		this.icon = beanModel.getRowIcon();
		if (beanModel.getSelectIndex() != -1) {
			fixedSizeColumn(getColumnModel().getColumn(convertColumnIndexToView(beanModel.getSelectIndex())), 44);			
		}
		fixedSizeColumn(getColumnModel().getColumn(convertColumnIndexToView(beanModel.getStartIndex()-1)), 30);
		
		int columnCount = beanModel.getColumnCount();
		for (int i = 0; i < columnCount; i++) {
			int viewIndex = convertColumnIndexToView(i);
			if (viewIndex == -1) {
				continue;
			}
			TableColumn beanColumn = getColumnModel().getColumn(viewIndex);
			
			int preferredWidth = beanModel.getPreferredWidth(i);
			if (preferredWidth != -1) {
				beanColumn.setPreferredWidth(preferredWidth);
			}
		}
	}
	
	private void updateRowSorter() {
		if (beanModel == null) {
			return;
		}
		int columnCount = beanModel.getColumnCount();
		for (int i = 0; i < columnCount; i++) {
			int viewIndex = convertColumnIndexToView(i);
			Class<?> columnClass = getColumnClass(viewIndex);
			if (columnClass != null && Date.class.isAssignableFrom(columnClass)) {
				assignDefaultComparator(i);
			}
		}
		Enum<?> sortColumn = beanModel.getSortColumn();
		if (sortColumn != null) {
			int sortColumnIndex = beanModel.convertToIndex(sortColumn);
			RowSorter<? extends TableModel> sorter = getRowSorter();
			if (sorter != null && sorter.getModel() != null) {
				DefaultRowSorter<?, ?> castedSorter = ((DefaultRowSorter<?, ?>)sorter);
				if (castedSorter.isSortable(sortColumnIndex)) {
					List<? extends RowSorter.SortKey> keys= Arrays.asList(new RowSorter.SortKey(sortColumnIndex, SortOrder.ASCENDING));
					castedSorter.setSortKeys(keys);
				}
			}
		}
		
	}
	
	private void assignDefaultComparator(int index) {
		if (index >= getColumnCount() || index< 0) {
			return;
		}
		RowSorter<? extends TableModel> rowSorter= getRowSorter();
		if (rowSorter != null && rowSorter.getModel() != null) {
			((DefaultRowSorter<?, ?>)rowSorter).setComparator(index, new ComparableComparator());
		}
		
	}
	
	private void fixedSizeColumn(TableColumn column, int size) {
		if (column != null) {
			column.setMinWidth(size);
			column.setMaxWidth(size);
			column.setPreferredWidth(size);
			column.setResizable(false);
		}
		
	}
	
	@Override
	public void createDefaultColumnsFromModel() {
		super.createDefaultColumnsFromModel();
		if (beanModel == null) {
			return;
		}
	}
	
	@SuppressWarnings("unchecked")
	public BeanTableModel<E> getBeanModel() {
		return (BeanTableModel<E>)getModel();
	}
	
	@Override
	public TableCellEditor getCellEditor(int row, int column) {
		int modelColumn = convertColumnIndexToModel(column);
		if (getBeanModel().isSelectionColumn(modelColumn)) {
			return booleanEditor;
		}
		return super.getCellEditor(row, column);
	}
	
	@Override
	public TableCellRenderer getCellRenderer(int row, int column) {
		int modelColumn = convertColumnIndexToModel(column);
		if (getBeanModel().isSelectionColumn(modelColumn)) {
			return booleanRenderer;
		} else if (getBeanModel().isIconColumn(modelColumn)) {
			return iconRenderer;
		} else if (getColumnClass(column) == Boolean.class) {
			return super.getCellRenderer(row, column);
		}
		return beanRenderer;
	}
	
	public void setSelectedElements(List<E> elements) {
		if(beanModel == null) {
			return;
		}
		if (beanModel.getSelectionMode() == SelectionMode.DEFAULT) {
			getSelectionModel().clearSelection();
			int smallestIndex = Integer.MAX_VALUE;
			for (E e : elements) {
				int modelRowIndex = beanModel.getBeans().indexOf(e);
				if (modelRowIndex != -1) {
					int rowIndex = convertRowIndexToView(modelRowIndex);
					if (rowIndex != -1) {
						if (rowIndex < smallestIndex) {
							smallestIndex = rowIndex;
						}
						getSelectionModel().addSelectionInterval(rowIndex, rowIndex);
					}
				}
			}
			if (smallestIndex != Integer.MAX_VALUE && getColumnCount() > 0) {
				scrollRectToVisible(getCellRect(smallestIndex, 0, true));
			}
		} else {
			beanModel.setSelectedElements(elements);
			List<E> newlySelected = beanModel.getSelectedElements();
			int smallestRowIndex = Integer.MAX_VALUE;
			for (E e : newlySelected) {
				int modelIndex = beanModel.getBeans().indexOf(e);
				if (modelIndex != -1) {
					int viewIndex = convertRowIndexToView(modelIndex);
					if (viewIndex != -1) {
						if (viewIndex < smallestRowIndex) {
							smallestRowIndex = viewIndex;
						}
					}
				}
			}
			if (smallestRowIndex != Integer.MAX_VALUE && getColumnCount() > 0) {
				scrollRectToVisible(getCellRect(smallestRowIndex, 0, true));
			}
		}
	}
	
	@SuppressWarnings("unchecked")
	public List<E> getSelectedElements() {
		if (beanModel == null) {
			return Collections.EMPTY_LIST;
		}
		if (beanModel.getSelectionMode() == SelectionMode.DEFAULT) {
			int[] rows = getSelectedRows();
			if (rows == null || rows.length == 0) {
				return null;
			}
			List<E> selected = new ArrayList<E>();
			if (getRowCount() == 0) {
				return selected;
			}
			
			for (int i : rows) {
				if (i < 0) {
					continue;
				}
				int modelIndex = convertRowIndexToModel(i);
				if (modelIndex != -1) {
					E bean = getBeanModel().getBeans().get(modelIndex);
					if (bean != null) {
						selected.add(bean);
					}
				}
			}
			return selected;

		} else {
			return beanModel.getSelectedElements();
		}
	}
	
	public E getSelectedElement() {
		List<E> elements = getSelectedElements();
		if (elements != null && elements.size() > 0) {
			return elements.get(0);
		}
		return null;
	}
	
	private static final Color SECONDARY_TABLE_COLOR = Color.decode("#d5e1ff");
	
	private void applyStyle(JLabel label, Object value, int viewRow, int viewColumn, boolean selected, boolean focus) {
		if (beanModel == null) {
			return;
		}
		int row = convertRowIndexToModel(viewRow);
		int column = convertColumnIndexToModel(viewColumn);
		
		label.setText(formatValue(value, beanModel.getFormat(row, column)));
		if (value instanceof BigDecimal) {
			label.setHorizontalAlignment(JLabel.TRAILING);
		} else {
			label.setHorizontalAlignment(JLabel.LEADING);
		}
		
		Color background = beanModel.getBackgroundColor(row, column);
		if (background != null && !selected && !focus) {
			label.setBackground(background);
		} else {
			if (selected) {
				label.setBackground(getSelectionBackground());
			} else if (viewRow % 2 == 0 && getLastLineColor() == null) {
				label.setBackground(Color.WHITE);
			} else {
				if (getLastLineColor() == null) {
					label.setBackground(SECONDARY_TABLE_COLOR);
				} else {
					if (getRowCount()-1 == row) {
						label.setBackground(getLastLineColor());
					} else {
						label.setBackground(Color.WHITE);
					}
				}
			}
		}
		Color foreground = beanModel.getForegroundColor(row, column);
		if (foreground != null) {
			label.setForeground(foreground);
		} else {
			if (selected) {
				label.setForeground(getSelectionForeground());
			} else {
				label.setForeground(Color.BLACK);
			}
		}
		
		boolean bold = beanModel.isBold(row, column);
		if (bold) {
			label.setFont(label.getFont().deriveFont(Font.BOLD));
		} else {
			label.setFont(label.getFont().deriveFont(Font.PLAIN));
		}
	}
	
	
	private String formatValue(Object value, Object formatObject) {
		if (value instanceof Date) {
			if (formatObject == null) {
				return FormatUtilities.formatDateTime((Date)value);
			} else {
				return ((DateFormat)formatObject).format((Date)value);
			}
		}
		if (value instanceof BigDecimal) {
			NumberFormat format = (NumberFormat)formatObject;
			if(formatObject == null) {
				return FormatUtilities.formatMoney((BigDecimal)value);
			} else {
				return format.format(value);
			}
		}
		return value == null ? "" : value.toString();
	}
	
	private class SelectionAdapter implements MouseListener, MouseMotionListener, KeyListener{
		public void mouseMoved(MouseEvent e) {
			if (hoverEffect) {
				int row = rowAtPoint(e.getPoint());
				if (row != -1) {
					getSelectionModel().setSelectionInterval(row, row);
				}
			}
		}
		public void mouseReleased(MouseEvent e) {
			if (e.getButton() != MouseEvent.BUTTON1) {
				return;
			}
			int column = columnAtPoint(e.getPoint());
			updateSelection(column);
		}
		public void keyReleased(KeyEvent e) {
			if (e.getKeyCode() == KeyEvent.VK_SPACE && e.getModifiers() == 0) {
				updateSelection(getSelectedColumn());
			}
		}
		
		private void updateSelection(int currentColumn) {
			
			int[] rows = getSelectedRows();
			int column = currentColumn;
			
			int modelIndex = getBeanModel().getSelectIndex();
			int viewIndex = convertColumnIndexToView(modelIndex);
			
			int leadRow = getSelectionModel().getLeadSelectionIndex();
			
			if (viewIndex == column) {
				return;
			}
			
			Boolean select = (Boolean)getValueAt(leadRow, viewIndex);
			if (select == null) {
				select = Boolean.TRUE;
			} else {
				select = select.booleanValue() ? Boolean.FALSE : Boolean.TRUE;
			}
			for (int i = 0; i < rows.length; i++) {
				int j = rows[i];
				if (!isCellEditable(j, currentColumn)) {
					setValueAt(select, j, viewIndex);
				}
			}
			beanModel.fireTableDataChanged();
			if (rows.length != 0) {
				getSelectionModel().setSelectionInterval(rows[0], rows[rows.length-1]);
			}
		}
		@Override
		public void keyTyped(KeyEvent e) {}
		@Override
		public void keyPressed(KeyEvent e) {}
		@Override
		public void mouseDragged(MouseEvent e) {}
		@Override
		public void mouseClicked(MouseEvent e) {}
		@Override
		public void mousePressed(MouseEvent e) {}
		@Override
		public void mouseEntered(MouseEvent e) {
		}
		@Override
		public void mouseExited(MouseEvent e) {
			getSelectionModel().clearSelection();
		}
	}
	
	
	private class BooleanEditor extends DefaultCellEditor {
		public BooleanEditor() {
			super(new JCheckBox());
			JCheckBox checkBox = (JCheckBox) getComponent();
			checkBox.setHorizontalAlignment(JCheckBox.CENTER);
			clickCountToStart = 1;

		}
	}
	
	private static class BooleanRenderer extends JCheckBox implements
			TableCellRenderer, UIResource {
		private static final Border noFocusBorder = new EmptyBorder(1, 1, 1, 1);

		public BooleanRenderer() {
			super();
			setHorizontalAlignment(JLabel.CENTER);
			setBorderPainted(true);
		}

		public Component getTableCellRendererComponent(JTable table,
				Object value, boolean isSelected, boolean hasFocus, int row,
				int column) {
			setOpaque(true);
			if (isSelected) {
				setForeground(table.getSelectionForeground());
				setBackground(table.getSelectionBackground());
			} else {
				setForeground(table.getForeground());
				setBackground(table.getBackground());
			}
			putClientProperty("$table", table);
			setSelected((value != null && ((Boolean) value).booleanValue()));

			if (hasFocus) {
				setBorder(UIManager.getBorder("Table.focusCellHighlightBorder"));
			} else {
				setBorder(noFocusBorder);
			}

			return this;
		}
	}
	
	private class BeanCellRenderer extends DefaultTableCellRenderer {
		
		private static final long serialVersionUID = -3871066563039327498L;

		@Override
		public Component getTableCellRendererComponent(JTable table,
				Object value, boolean isSelected, boolean hasFocus, int row,
				int column) {
			Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus,
					row, column);
			if (c instanceof JLabel) {
				applyStyle((JLabel)c, value, row, column, isSelected, hasFocus);
			}
			return c;
		}

	}
	
	private class IconCellRenderer extends BeanCellRenderer {
		
		private static final long serialVersionUID = 7996598454475410112L;

		@Override
		public Component getTableCellRendererComponent(JTable table,
				Object value, boolean isSelected, boolean hasFocus, int row,
				int column) {
			Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus,
					row, column);
			if (c instanceof JLabel) {
				JLabel label = (JLabel)c;
				label.setText("");
				if (value != null) {
					label.setIcon((ImageIcon)value);
				} else if (icon != null) {
					label.setIcon(icon);
				}
			}
			return c;
		}
		
	}
	
	public void setValueAt(Object aValue, int row, int column) {
		try {
			super.setValueAt(aValue, row, column);
		} catch (Exception e) {
			ExceptionDialog.handleException(this, e);
		}
		for (int i = 0; i < changeListeners.size(); i++) {
			changeListeners.get(i).changed();
		}
	};
	
	private List<ChangeListener> changeListeners = new ArrayList<BeanTable.ChangeListener>();
	public void addChangeListener(ChangeListener listener) {
		changeListeners.add(listener);
	}
	public void removeChangeListener(ChangeListener listener) {
		changeListeners.remove(listener);
	}
	
	public static interface ChangeListener {
		void changed();
	}
	
}
